use structopt::StructOpt;
use std::net::TcpStream;
use std::io::{Read, Error as IoError, ErrorKind};
use std::str;
use serde_json::{self, Value as JsonValue};
use reqwest;
use http::StatusCode;
use std::fs;
use std::error::Error;
use serde::Deserialize;

const OUT_IP_URL: &str = "ns1.dnspod.net:6666";
const DNS_LIST_URL: &str = "https://dnsapi.cn/Record.List";
const DNS_UPDATE_URL: &str = "https://dnsapi.cn/Record.Modify";
const TEMP_MARK_FILE: &str = "/tmp/out_ip.mark";

#[derive(Debug, StructOpt)]
struct Cli {
    #[structopt(long = "domain", short = "d")]
    domain: String,

    #[structopt(long = "subdomain", short = "s")]
    sub_domain: String,

    #[structopt(long = "token", short = "k")]
    token: String,
}

#[derive(Debug)]
struct RecordUpdateParam {
    token: String,
    domain_id: String,
    sub_domain: String,
    record_id: String,
    record_type: String,
    record_line_id: String,
    value: String
}

struct RecordGetParam<'a> {
    token: & 'a str,
    domain: & 'a str,
    sub_domain: & 'a str,
    url: & 'a str,
    length: & 'a str,
    record_type: & 'a str,
    format: & 'a str
}

#[derive(Deserialize, Debug)]
struct DomainFail {
    status: Status
}

#[derive(Deserialize, Debug)]
struct DomainRecord{
    status: Status,
    domain: Domain,
    records: Vec<Record>,
} 

#[derive(Deserialize, Debug)]
struct Status {
    code: String,
    message: String,
}

#[derive(Deserialize, Debug)]
struct Domain {
    id: String,
}

#[derive(Deserialize, Debug)]
struct Record {
    id: String,
    line_id: String,
    #[serde(rename = "type")]
    r_type: String,
    value: String
}



fn main() {
    let args = Cli::from_args();
    let mut local_ip: String = "".to_owned();
    match fs::read_to_string(TEMP_MARK_FILE) {
        Err(e) => {
            if e.kind() == ErrorKind::PermissionDenied {
                println!("permission denied when open mark file: {}", TEMP_MARK_FILE);
                return;
            }
        },
        Ok(content) => local_ip = content.trim().to_owned(),
    }
    let current_ip = get_out_ip();
    let c_ip = match current_ip {
        Err(e) => {
            println!("get out ip error: {}", e);
            return;
        },
        Ok(cip) => {
            if cip == local_ip {
                println!("ip not changed, exit");
                return;
            } else {
                cip
            }
        },
    };

    let get_record_param = RecordGetParam {
        token: &args.token,
        domain: &args.domain,
        sub_domain: &args.sub_domain,
        url: DNS_LIST_URL,
        length: "1",
        record_type: "A",
        format: "json",
    };

    let update_param_wrap = get_record(&get_record_param);
    let mut update_param = match update_param_wrap {
        Err(e) => {
            println!("erro when get dns record: {}", e);
            return;
        },
        Ok(upp) => upp,
    };
    
    println!("{:?}", update_param);
    if update_param.value == c_ip {
        println!("record value is same as out ip");
        return;
    }

    update_param.value = c_ip.clone();
    match update_record(update_param) {
        Err(e) => println!("error when updating dns record: {}", e),
        Ok(_) => {
            println!("successfully updated dns record");
            match fs::write(TEMP_MARK_FILE, c_ip) {
                Err(e) => println!("err when write ip: {}", e),
                Ok(_) => println!("write ip locallly"),
            }

        }
    }
}

fn get_out_ip() -> Result<String, Box<dyn Error>> {
    let mut stream = TcpStream::connect(OUT_IP_URL)?;
    let mut read_buff = [0; 128];
    let read_nu = stream.read(&mut read_buff)?;
    let read_content = read_buff.get(0..read_nu).ok_or(IoError::new(ErrorKind::Other, "get read buff error"))?;
    let content = String::from_utf8(read_content.to_vec())?;
    return Ok(content);
}
fn get_record(rp: &RecordGetParam) -> Result<RecordUpdateParam, Box<dyn Error>> {
    let params = [("login_token", rp.token), ("domain", rp.domain),
     ("sub_domain", rp.sub_domain), ("length", rp.length), ("record_type", rp.record_type),
     ("format", rp.format)];
    let client = reqwest::blocking::Client::new();
    let res = client.post(rp.url)
        .form(&params)
        .send()?;
    if res.status() != StatusCode::OK {
        return Err(IoError::new(ErrorKind::Other, "status code is not 200"))?;
    }
    let body = res.bytes()?;

    let message: DomainFail = serde_json::from_slice(&body)?;
    if message.status.code.ne("1") {
        return Err(IoError::new(ErrorKind::Other, message.status.message))?;
    }
    let value: DomainRecord = serde_json::from_slice(&body)?;
    return Ok(RecordUpdateParam{
        token: rp.token.to_owned(),
        domain_id: value.domain.id,
        sub_domain: rp.sub_domain.to_owned(),
        record_id: value.records[0].id.clone(),
        record_type: value.records[0].r_type.clone(),
        record_line_id: value.records[0].line_id.clone(),
        value: value.records[0].value.clone(),
    })
}
fn update_record(rp: RecordUpdateParam) -> Result<(), Box<dyn Error>> {
    let params = [("login_token", rp.token), ("domain_id", rp.domain_id),
        ("sub_domain", rp.sub_domain), ("record_id", rp.record_id),
        ("record_type", rp.record_type), ("record_line_id", rp.record_line_id),
        ("format", "json".to_owned()), ("value", rp.value)];
    let client = reqwest::blocking::Client::new();
    let res = client.post(DNS_UPDATE_URL)
        .form(&params)
        .send()?;
    if res.status() != StatusCode::OK {
        return Err(IoError::new(ErrorKind::Other, "status code is not 200"))?;
    }
    let body = res.bytes()?;
    println!("{:?}", body);
    let value: JsonValue = serde_json::from_slice(&body)?;
    return if value["status"]["code"].as_str().unwrap_or("1000") == "1" {
        Ok(())
    } else {
        Err(IoError::new(ErrorKind::Other, value["status"]["message"].to_string()))?
    };
}
